/**
 * Project: d20Engine
 * Created: Oct 21, 2006 by bebopJMM
 */
package org.rollinitiative.d20;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * AdjustableValue manages a single score, such as an ability or hitPoints. It tracks all
 * adjustments, however temporary or permanent, against the base score. It notifies all subscribers
 * to changes in the value being managed.
 * 
 * @author bebopjmm
 * 
 */
public class AdjustableValue implements AdjustmentListener
{
   static final Log LOG = LogFactory.getLog(AdjustableValue.class);

   public static final int DEFAULT_VALUE = 0;

   /**
    * The baseValue is the raw score without any modifiers.
    */
   private int baseValue;

   /**
    * The currentValue is the ability score with all currently applicable modifiers
    */
   private int currentValue;
   
   /**
    * Optional identifying name for the adjustable value (useful for debugging)
    */
   private String name = "";

   /**
    * The current set of adjustments effecting this adjustable value.
    */
   List<Adjustment> adjustments;

   /**
    * Subscribers to changes in currentValue
    */
   List<AdjustableValueListener> subscribers;


   /**
    * Instantiates a new AdjustableValue, setting both base and current values to the provided
    * baseValue.
    * 
    * @param baseValue
    * 
    * @since incr-0.1
    */
   public AdjustableValue(int baseValue)
   {
      this.baseValue = baseValue;
      this.currentValue = baseValue;
      adjustments = new ArrayList<Adjustment>();
      subscribers = new ArrayList<AdjustableValueListener>();
   }

   

   /**
    * @return the name
    */
   public String getName()
   {
      return name;
   }



   /**
    * @param name the name to set
    */
   public void setName(String name)
   {
      this.name = name;
   }



   /**
    * @return the baseValue
    * @since incr-0.1
    */
   public int getBase()
   {
      return baseValue;
   }


   /**
    * @param baseValue the baseValue to set
    * @since incr-0.1
    */
   public synchronized void setBase(int baseValue)
   {
      this.baseValue = baseValue;
      recalcValue();
      notifyOfChange();
   }


   /**
    * @return the current ability value
    * @since incr-0.1
    */
   public synchronized int getCurrent()
   {
      return currentValue;
   }


   /**
    * Adds the adjustment to the value calculation set, updating the currentValue and notifying all
    * subscribed listeners. No action is taken if the provided Adjustment is already part of the
    * list.
    * 
    * @param adjustment new Adjustment to include in value calculation
    */
   public synchronized void addAdjustment(Adjustment adjustment)
   {
      if (adjustments.contains(adjustment)) {
         LOG.warn(name + ": Attempt to add duplicate ability adjustment: " + adjustment.getPedigree().getLocalPart());
         return;
      }
      LOG.debug(name + ": Adding Adjustment: " + adjustment.getPedigree().getLocalPart() + " of value: "
            + adjustment.getValue());
      adjustments.add(adjustment);
      adjustment.subscribe(this);
      currentValue += adjustment.getValue();
      LOG.debug(name + ": Current value =" + currentValue);
      notifyOfChange();
   }


   /**
    * Removes the adjustment from the value calculation set, updating the currentValue and notifying
    * all subscribed listeners. No action is taken if the provide Adjustment is not part of the
    * list.
    * 
    * @param adjustment Adjustment to be removed from value calculation.
    */
   public synchronized void removeAdjustment(Adjustment adjustment)
   {
      if (!adjustments.contains(adjustment)) {
         LOG.warn(name + ": Attempt to remove adjustment not associated to ability: "
               + adjustment.getPedigree().getLocalPart());
         return;
      }
      LOG.debug(name + ": Removing Adjustment: " + adjustment.getPedigree().getLocalPart() + " of value: "
            + adjustment.getValue());
      adjustments.remove(adjustment);
      adjustment.unsubscribe(this);
      currentValue -= adjustment.getValue();
      LOG.debug(name + ": Current value =" + currentValue);
      notifyOfChange();
   }


   /**
    * @return the array of all Adjustments currently used in the value calculation.
    */
   public synchronized Adjustment[] getAdjustments()
   {
      Adjustment[] adjArray = new Adjustment[adjustments.size()];
      return adjustments.toArray(adjArray);
   }


   public void subscribe(AdjustableValueListener listener)
   {
      synchronized (subscribers) {
         if (!subscribers.contains(listener)) {
            subscribers.add(listener);
         }
      }
   }


   public void unsubscribe(AdjustableValueListener listener)
   {
      synchronized (subscribers) {
         if (subscribers.contains(listener)) {
            subscribers.remove(listener);
         }
      }
   }


   /*
    * (non-Javadoc)
    * 
    * @see org.rollinitiative.d20.AdjustmentListener#valueChanged(org.rollinitiative.d20.Adjustment)
    */
   @Override
   public void valueChanged(Adjustment adjustment)
   {
      int oldVal = currentValue;
      recalcValue();
      if (oldVal != currentValue) {
         notifyOfChange();
      }
      else {
         LOG.debug(name + ": No change in currentValue, not notifying subscribers");
      }
   }


   protected void recalcValue()
   {
      LOG.debug(name + ": recalculating current value: baseValue + sum of adjustments");
      currentValue = baseValue;
      for (Adjustment adjustment : adjustments) {
         currentValue += adjustment.getValue();
      }
   }


   protected void notifyOfChange()
   {
      synchronized (subscribers) {
         LOG.debug(name + ": Notifying subcribers of currentValue change, totalSubscribers = "
               + subscribers.size());
         for (AdjustableValueListener subscriber : subscribers) {
            subscriber.valueChanged(this);
         }
      }
   }
}
